import { IOSConfig, type ModPlatform, type XcodeProject } from 'expo/config-plugins';
import fs from 'fs/promises';
import path from 'path';

import type { WorkingDirectories } from './workingDirectories';

interface Params {
  projectRoot: string;
  workingDirectories: WorkingDirectories;
  platform: ModPlatform;
  backup: boolean;
}

type BackupFileMappings = Record<string, string>;

/**
 * Normalize the native projects to minimize the diff between prebuilds.
 */
export async function normalizeNativeProjectsAsync(params: Params): Promise<BackupFileMappings> {
  if (params.platform === 'ios') {
    return await normalizeIosProjectsAsync(params);
  }
  return {};
}

/**
 * Revert the changes made by `normalizeNativeProjectsAsync()`.
 */
export async function revertNormalizeNativeProjectsAsync(
  backupFileMappings: BackupFileMappings
): Promise<void> {
  await Promise.all(
    Object.entries(backupFileMappings).map(([originalPath, backupPath]) =>
      fs.copyFile(backupPath, originalPath)
    )
  );
}

/**
 * `normalizeNativeProjectsAsync()` implementation for iOS.
 */
async function normalizeIosProjectsAsync({
  projectRoot,
  workingDirectories,
  backup,
}: Params): Promise<BackupFileMappings> {
  const backupFileMappings: BackupFileMappings = {};

  // Normalize the pbxproj file since it has some dynamic IDs that change between prebuilds.
  const xcodeProjectPath = IOSConfig.Paths.getPBXProjectPath(projectRoot);
  const xcodeProjectPathBackup = path.join(
    workingDirectories.tmpDir,
    path.basename(xcodeProjectPath)
  );
  if (backup) {
    await fs.copyFile(xcodeProjectPath, xcodeProjectPathBackup);
    backupFileMappings[xcodeProjectPath] = xcodeProjectPathBackup;
  }
  await normalizeXcodeProjectAsync(projectRoot, xcodeProjectPath);

  return backupFileMappings;
}

/**
 * Normalize the Xcode project file by removing some dynamic values.
 * - Remove the `noop-file.swift` file that is generated by the prebuild process.
 * - Remove the Swift bridging header file that is generated by the prebuild process.
 * - Remove the `ExpoModulesProvider.swift` that is generated by expo-modules-autolinking and `pod install`.
 * - Remove the `[Expo] Configure project` build phase that is generated by expo-modules-autolinking and `pod install`.
 * - Remove the CocoaPods build phases that are generated by `pod install`.
 */
async function normalizeXcodeProjectAsync(
  projectRoot: string,
  xcodeProjectPath: string
): Promise<void> {
  const projectName = IOSConfig.XcodeUtils.getProjectName(projectRoot);
  const project = IOSConfig.XcodeUtils.getPbxproj(projectRoot);

  const mainAppGroup = findXcodeProjectMainAppGroupKey(project, projectName);
  project.removeSourceFile('noop-file.swift', null, mainAppGroup);
  project.removeSourceFile('ExpoModulesProvider.swift', null, mainAppGroup);
  project.removeSourceFile(`${projectName}-Bridging-Header.h`, null, mainAppGroup);
  removeXcodeProjectBuildPhase(project, '[Expo] Configure project');
  removeXcodeProjectBuildPhase(project, '[CP] Embed Pods Frameworks');
  removeXcodeProjectBuildPhase(project, '[CP] Check Pods Manifest.lock');
  removeXcodeProjectBuildPhase(project, '[CP] Copy Pods Resources');

  await fs.writeFile(xcodeProjectPath, project.writeSync(), 'utf8');
}

/**
 * Find the main app group key in the Xcode project.
 */
function findXcodeProjectMainAppGroupKey(project: XcodeProject, projectName: string): string {
  const targetGroupKey = project.findPBXGroupKey({ name: projectName });

  // Sanity check
  const { firstProject } = project.getFirstProject();
  const mainGroup = project.getPBXGroupByKey(firstProject.mainGroup);
  // @ts-expect-error
  const targetGroupKey2 = mainGroup?.children.find(({ comment }) => comment === projectName)?.value;
  if (!targetGroupKey || targetGroupKey !== targetGroupKey2) {
    throw new Error('Unable to find the main app group key');
  }

  return targetGroupKey;
}

/**
 * Remove a shell script build phase from the Xcode project.
 */
function removeXcodeProjectBuildPhase(project: XcodeProject, name: string) {
  const [, nativeTarget] = IOSConfig.Target.findFirstNativeTarget(project);
  // @ts-expect-error
  const index = nativeTarget.buildPhases.findIndex((item) => item.comment === name);
  if (index < 0) {
    return;
  }
  const buildPhaseId = nativeTarget.buildPhases[index].value;
  nativeTarget.buildPhases.splice(index, 1);

  const buildPhases = project.getPBXObject('PBXShellScriptBuildPhase') as Record<string, any>;
  delete buildPhases[buildPhaseId];
  delete buildPhases[`${buildPhaseId}_comment`];
}
